var ejs = require('ejs'),
	path = require('path'),
	fs = require('fs-extra'),
	walkSync = require('walk-sync'),
	vm = require('vm'),
	babel = require('@babel/core'),
	async = require('async'),

	// alloy requires
	_ = require('lodash'),
	logger = require('../../logger'),
	U = require('../../utils'),
	tiapp = require('../../tiapp'),
	CONST = require('../../common/constants'),
	platforms = require('../../../platforms/index'),

	// alloy compiler requires
	CU = require('./compilerUtils'),
	styler = require('./styler'),
	sourceMapper = require('./sourceMapper'),
	CompilerMakeFile = require('./CompilerMakeFile'),
	BuildLog = require('./BuildLog'),
	Orphanage = require('./Orphanage');

var alloyRoot = path.join(__dirname, '..', '..'),
	viewRegex = new RegExp('\\.' + CONST.FILE_EXT.VIEW + '$'),
	controllerRegex = new RegExp('\\.' + CONST.FILE_EXT.CONTROLLER + '$'),
	modelRegex = new RegExp('\\.' + CONST.FILE_EXT.MODEL + '$'),
	compileConfig = {},
	otherPlatforms,
	buildPlatform,
	titaniumFolder,
	buildLog,
	theme,
	platformTheme,
	widgetIds = [];

var times = {
	first: null,
	last: null,
	msgs: []
};

var fileRestrictionUpdatedFiles = [],
	restrictionSkipOptimize = false;

//////////////////////////////////////
////////// command function //////////
//////////////////////////////////////
module.exports = function(args, program) {
	BENCHMARK();
	var alloyConfig = {},
		compilerMakeFile,
		restrictionPath,

		// NOTE: the following line creates the empty Resources/app.js file
		paths = U.getAndValidateProjectPaths(
			program.outputPath || args[0] || process.cwd()
		);

	// Initialize modules used throughout the compile process
	buildLog = new BuildLog(paths.project);
	tiapp.init(path.join(paths.project, 'tiapp.xml'));

	// validate the current Titanium SDK version, exit on failure
	tiapp.validateSdkVersion();

	// construct compiler config from command line config parameters
	// and print the configuration data
	logger.debug('----- CONFIGURATION -----');
	if (program.config && _.isString(program.config)) {
		logger.debug('raw config = "' + program.config + '"');
		_.each(program.config.split(','), function(v) {
			var parts = v.split('=');
			if (alloyConfig[parts[0]]) {
				alloyConfig[parts[0]] = [].concat(alloyConfig[parts[0]], parts[1]);
			} else {
				alloyConfig[parts[0]] = parts[1];
			}
			logger.debug(parts[0] + ' = ' + parts[1]);
		});
	}
	if (program.platform) {
		logger.debug('platform = ' + program.platform);
		alloyConfig.platform = program.platform;
	}
	if (!alloyConfig.deploytype) {
		alloyConfig.deploytype = 'development';
		logger.debug('deploytype = ' + alloyConfig.deploytype);
	}
	logger.debug('project path = ' + paths.project);
	logger.debug('app path = ' + paths.app);
	logger.debug('');

	// make sure a platform was specified
	buildPlatform = alloyConfig.platform;
	if (!buildPlatform) {
		U.die([
			'You must define a target platform for the alloy compile command',
			'  Ex. "alloy compile --config platform=ios"'
		]);
	}
	titaniumFolder = platforms[buildPlatform].titaniumFolder;
	otherPlatforms = _.without(CONST.PLATFORM_FOLDERS, titaniumFolder);

	// check the platform and i18n to see if it was generated by us last time
	var destI18NDir = path.join(paths.project, 'i18n');
	var destPlatformDir = path.join(paths.project, 'platform', buildPlatform === 'iphone' ? 'ios' : buildPlatform);
	if (fs.existsSync(destI18NDir) && !fs.existsSync(path.join(destI18NDir, 'alloy_generated'))) {
		U.die([
			'Detected legacy "/i18n" directory in project directory.',
			'Please move the "/i18n" directory to "/app/i18n" for Alloy 1.8.0 or later.'
		]);
	}
	if (fs.existsSync(destPlatformDir) && !fs.existsSync(path.join(destPlatformDir, 'alloy_generated'))) {
		U.die([
			'Detected legacy "/platform" directory in project directory.',
			'Please move the "/platform" directory to "/app/platform" for Alloy 1.8.0 or later.'
		]);
	}

	// check that the .gitignore is looking good
	var gitignoreFile = path.join(paths.project, '.gitignore');
	if (fs.existsSync(gitignoreFile)) {
		var folders = {
			'/i18n': false,
			'/platform': false,
			'/Resources': false
		};
		fs.readFileSync(gitignoreFile).toString().split('\n').forEach(function (line) {
			line = line.trim();
			if (/^\/?i18n(\/.*)?$/.test(line)) {
				folders['/i18n'] = true;
			} else if (/^\/?platform(\/.*)?$/.test(line)) {
				folders['/platform'] = true;
			} else if (/^\/?Resources(\/.*)?$/.test(line)) {
				folders['/Resources'] = true;
			}
		});
		var warned = false;
		Object.keys(folders).some(function (dir) {
			if (!folders[dir]) {
				logger.warn('Generated "' + dir + '" directory is not ignored by Git, please add it to your .gitignore');
				warned = true;
			}
		});
		warned && logger.debug();
	}

	// allow to filter the file to compile
	if (!alloyConfig.file) {
		restrictionPath = null;
	} else {
		restrictionPath = _.map([].concat(alloyConfig.file), function (file) {
			return path.join(paths.project, file);
		});
	}

	// create compile config from paths and various alloy config files
	logger.debug('----- CONFIG.JSON -----');
	// NOTE: the following line creates the Resources/alloy/CFG.js and Resources/<platform-name>/alloy/CFG.js
	compileConfig = CU.createCompileConfig(paths.app, paths.project, alloyConfig, buildLog);
	theme = compileConfig.theme;
	platformTheme = buildLog.data[buildPlatform] ? buildLog.data[buildPlatform]['theme'] : '';

	buildLog.data.themeChanged = theme !== platformTheme;
	buildLog.data.theme = theme;
	// track whether deploy type has changed since previous build
	buildLog.data.deploytypeChanged = buildLog.data.deploytype !== alloyConfig.deploytype;
	buildLog.data.deploytype = alloyConfig.deploytype;
	logger.debug('');

	// wipe the controllers, models, and widgets
	logger.debug('----- CLEANING RESOURCES -----');
	var orphanage = new Orphanage(paths.project, buildPlatform, {
		theme: theme,
		adapters: compileConfig.adapters
	});
	orphanage.clean();
	logger.debug('');

	// process project makefiles
	compilerMakeFile = new CompilerMakeFile();
	var alloyJMK = path.resolve(path.join(paths.app, 'alloy.jmk'));
	if (fs.existsSync(alloyJMK)) {
		logger.debug('Loading "alloy.jmk" compiler hooks...');
		var script = vm.createScript(fs.readFileSync(alloyJMK), 'alloy.jmk');

		// process alloy.jmk compile file
		try {
			script.runInNewContext(compilerMakeFile);
			compilerMakeFile.isActive = true;
		} catch (e) {
			logger.error(e.stack);
			U.die('Project build at "' + alloyJMK + '" generated an error during load.');
		}

		compilerMakeFile.trigger('pre:load', _.clone(compileConfig));
		logger.debug('');
	}

	// create generated controllers folder in resources
	logger.debug('----- BASE RUNTIME FILES -----');
	U.installPlugin(path.join(alloyRoot, '..'), paths.project);

	// copy in all lib resources from alloy module, exclude backbone dir
	updateFilesWithBuildLog(
		path.join(alloyRoot, 'lib'),
		path.join(paths.resources, titaniumFolder),
		{
			rootDir: paths.project,
			filter: new RegExp('^alloy[\\/\\\\]backbone([\\/\\\\]|$)'),
			exceptions: _.map(_.difference(CONST.ADAPTERS, compileConfig.adapters), function(a) {
				return path.join('alloy', 'sync', a + '.js');
			}),
			restrictionPath: restrictionPath
		}
	);

	// Copy the version of backbone that is specified in config.json
	U.copyFileSync(
		path.join(
			alloyRoot, 'lib', 'alloy', 'backbone',
			(_.includes(CONST.SUPPORTED_BACKBONE_VERSIONS, compileConfig.backbone))
				? compileConfig.backbone
				: CONST.DEFAULT_BACKBONE_VERSION,
			'backbone.js'
		),
		path.join(paths.resources, titaniumFolder, 'alloy', 'backbone.js')
	);

	if (restrictionPath === null) {
		// Generate alloy.js from template
		var libAlloyJsDest = path.join(paths.resources, titaniumFolder, 'alloy.js');
		var pkginfo = require('pkginfo')(module, 'version');
		logger.trace('Generating ' + path.relative(titaniumFolder, libAlloyJsDest).yellow);
		fs.writeFileSync(
			libAlloyJsDest,
			ejs.render(
				fs.readFileSync(path.join(alloyRoot, 'template', 'lib', 'alloy.js'), 'utf8'),
				{ version: module.exports.version }
			)
		);
	}

	// NOTE: copies `common/constants.js` from Alloy into `<project-dir>/Resources/<platform>/alloy`
	updateFilesWithBuildLog(
		path.join(alloyRoot, 'common'),
		path.join(paths.resources, titaniumFolder, 'alloy'),
		{ rootDir: paths.project, restrictionPath: restrictionPath }
	);

	// create runtime folder structure for alloy
	_.each(['COMPONENT', 'WIDGET', 'RUNTIME_STYLE'], function(type) {
		var p = path.join(paths.resources, titaniumFolder, 'alloy', CONST.DIR[type]);
		fs.mkdirpSync(p);
	});

	// Copy in all developer assets, libs, and additional resources
	_.each(['ASSETS', 'LIB', 'VENDOR'], function(type) {
		updateFilesWithBuildLog(
			path.join(paths.app, CONST.DIR[type]),
			path.join(paths.resources, titaniumFolder),
			{
				rootDir: paths.project,
				themeChanged: buildLog.data.themeChanged,
				filter: new RegExp('^(?:' + otherPlatforms.join('|') + ')[\\/\\\\]'),
				exceptions: otherPlatforms,
				createSourceMap: (type === 'ASSETS') ? false : compileConfig.sourcemap,
				compileConfig: compileConfig,
				titaniumFolder: titaniumFolder,
				type: type,
				restrictionPath: restrictionPath
			}
		);
	});

	// copy in test specs if not in production
	if (alloyConfig.deploytype !== 'production') {
		updateFilesWithBuildLog(
			path.join(paths.app, 'specs'),
			path.join(paths.resources, titaniumFolder, 'specs'),
			{ rootDir: paths.project, restrictionPath: restrictionPath }
		);
	}

	var defaultIcons = ['DefaultIcon.png', 'DefaultIcon-' + buildPlatform + '.png'];

	// check theme for assets
	if (theme) {
		_.each(['ASSETS', 'LIB', 'VENDOR'], function(type) {
			var themeAssetsPath = path.join(paths.app, 'themes', theme, CONST.DIR[type]);
			if (fs.existsSync(themeAssetsPath)) {
				updateFilesWithBuildLog(
					themeAssetsPath,
					path.join(paths.resources, titaniumFolder),
					{
						rootDir: paths.project,
						themeChanged: buildLog.data.themeChanged,
						filter: new RegExp('^(?:' + otherPlatforms.join('|') + ')[\\/\\\\]'),
						exceptions: otherPlatforms,
						titaniumFolder: titaniumFolder,
						restrictionPath: restrictionPath
					}
				);
			}
		});

		// This is not ideal, but until the build system allows icons to be picked up in another location,
		// we need to do this for now.
		defaultIcons.forEach(function (file) {
			var themeIconFile = path.join(paths.app, 'themes', theme, file);
			var projectIconFile = path.join(paths.project, file);
			var appcDirIconFile = path.join(paths.app, file);

			if (fs.existsSync(themeIconFile)) {
				if (fs.existsSync(projectIconFile) && !fs.existsSync(appcDirIconFile)) {
					// 1st time theming defaulticon, copy icon file from root to app dir
					logger.debug('Use themed DefaultIcon, make a copy of the DefaultIcon file in app folder.');
					logger.debug('Moving ' + projectIconFile.yellow + ' --> ' + appcDirIconFile.yellow);
					U.copyFileSync(projectIconFile, appcDirIconFile);
				}

				logger.debug('Use themed DefaultIcon.');
				logger.debug('Copying ' + themeIconFile.yellow + ' --> ' + projectIconFile.yellow);
				fs.copySync(themeIconFile, projectIconFile, { preserveTimestamps: true });
			}
		});

	} else {
		defaultIcons.forEach(function (file) {
			var src = path.join(paths.app, file);
			var dest = path.join(paths.project, file);
			if (fs.existsSync(src))  {
				logger.debug('Copying ' + src.yellow + ' --> ' + dest.yellow);
				fs.copySync(src, dest, { preserveTimestamps: true });
			}
		});
	}

	function generateMessage(dir) {
		return 'This directory is generated from the "/app/' + dir + '" and "/app/theme/<name>/' + dir + '" directories.\n\n' +
			'Do not modify any files in this directory. Your changes will be lost on next build.\n\n' +
			'Please make sure "/' + dir + '" is added to your version control\'s ignore list (i.e. .gitignore).';
	}

	// copy the platform and theme platform directories
	var sourcePlatformDirs;
	if (buildPlatform === 'ios' || buildPlatform === 'iphone') {
		sourcePlatformDirs = [ 'platform/iphone', 'platform/ios' ];
		var iPhonePlatformDir = path.join(paths.project, 'platform', 'iphone');
		if (fs.existsSync(iPhonePlatformDir)) {
			logger.trace('Deleting ' + iPhonePlatformDir.yellow);
			fs.removeSync(iPhonePlatformDir);
		}
	} else {
		sourcePlatformDirs = [ 'platform/' + buildPlatform ];
	}
	if (fs.existsSync(destPlatformDir)) {
		logger.debug('Resetting ' + destPlatformDir.yellow);
		fs.removeSync(destPlatformDir);
	}
	fs.mkdirpSync(destPlatformDir);
	fs.writeFileSync(path.join(destPlatformDir, 'alloy_generated'), generateMessage('platform'));
	sourcePlatformDirs.forEach(function (dir) {
		var dirs = [ dir ];
		theme && dirs.push('themes/' + theme + '/' + dir);
		dirs.forEach(function (dir) {
			dir = path.join(paths.app, dir);
			if (fs.existsSync(dir)) {
				logger.debug('Copying ' + dir.yellow + ' --> ' + destPlatformDir.yellow);
				fs.copySync(dir, destPlatformDir, { preserveTimestamps: true });
			}
		});
	});
	logger.debug('');

	// copy the i18n and i18n platform directories
	var sourceI18NPaths = [ path.join(paths.app, 'i18n') ];
	if (theme) {
		sourceI18NPaths.push(path.join(paths.app, 'themes', theme, 'i18n'));
	}
	if (fs.existsSync(destI18NDir)) {
		logger.debug('Resetting ' + destI18NDir.yellow);
		fs.removeSync(destI18NDir);
	}
	fs.mkdirpSync(destI18NDir);
	fs.writeFileSync(path.join(destI18NDir, 'alloy_generated'), generateMessage('i18n'));
	sourceI18NPaths.forEach(function (dir) {
		if (fs.existsSync(dir)) {
			CU.mergeI18N(dir, destI18NDir, { override: true });
		}
	});
	logger.debug('');

	// trigger our custom compiler makefile
	if (compilerMakeFile.isActive) {
		compilerMakeFile.trigger('pre:compile', _.clone(compileConfig));
	}

	logger.info('----- MVC GENERATION -----');

	// create the global style, if it exists
	styler.setPlatform(buildPlatform);
	styler.loadGlobalStyles(paths.app, theme ? {theme:theme} : {});

	// Create collection of all widget and app paths
	var widgetDirs = U.getWidgetDirectories(paths.app);
	widgetDirs.push({ dir: path.join(paths.project, CONST.ALLOY_DIR) });

	// Process all models
	var models = processModels(widgetDirs);
	_.each(models, function(m) {
		CU.models.push(m);
	});

	// Create a regex for determining which platform-specific
	// folders should be used in the compile process
	var filteredPlatforms = _.reject(CONST.PLATFORM_FOLDERS_ALLOY, function(p) {
		return p === buildPlatform;
	});
	filteredPlatforms = _.map(filteredPlatforms, function(p) { return p + '[\\\\\\/]'; });
	var filterRegex = new RegExp('^(?:(?!' + filteredPlatforms.join('|') + '))');

	// don't process XML/controller files inside .svn folders (ALOY-839)
	var excludeRegex = new RegExp('(?:^|[\\/\\\\])(?:' + CONST.EXCLUDED_FILES.join('|') + ')(?:$|[\\/\\\\])');

	// Process all views/controllers and generate their runtime
	// commonjs modules and source maps.
	var tracker = {};
	_.each(widgetDirs, function(collection) {
		// generate runtime controllers from views
		var theViewDir = path.join(collection.dir, CONST.DIR.VIEW);
		if (fs.existsSync(theViewDir)) {
			_.each(walkSync(theViewDir), function(view) {
				view = path.normalize(view);
				if (viewRegex.test(view) && filterRegex.test(view) && !excludeRegex.test(view)) {
					// make sure this controller is only generated once
					var theFile = view.substring(0, view.lastIndexOf('.'));
					var theKey = theFile.replace(new RegExp('^' + buildPlatform + '[\\/\\\\]'), '');
					var fp = path.join(collection.dir, theKey);
					if (tracker[fp]) { return; }

					// generate runtime controller
					logger.info('[' + view + '] ' + (collection.manifest ? collection.manifest.id +
						' ' : '') + 'view processing...');
					parseAlloyComponent(view, collection.dir, collection.manifest, null, restrictionPath);
					tracker[fp] = true;
				}
			});
		}

		// generate runtime controllers from any controller code that has no
		// corresponding view markup
		var theControllerDir = path.join(collection.dir, CONST.DIR.CONTROLLER);
		if (fs.existsSync(theControllerDir)) {
			_.each(walkSync(theControllerDir), function(controller) {
				controller = path.normalize(controller);
				if (controllerRegex.test(controller) && filterRegex.test(controller) && !excludeRegex.test(controller)) {
					// make sure this controller is only generated once
					var theFile = controller.substring(0, controller.lastIndexOf('.'));
					var theKey = theFile.replace(new RegExp('^' + buildPlatform + '[\\/\\\\]'), '');
					var fp = path.join(collection.dir, theKey);
					if (tracker[fp]) { return; }

					// generate runtime controller
					logger.info('[' + controller + '] ' + (collection.manifest ?
						collection.manifest.id + ' ' : '') + 'controller processing...');
					parseAlloyComponent(controller, collection.dir, collection.manifest, true, restrictionPath);
					tracker[fp] = true;
				}
			});
		}
	});
	logger.info('');

	generateAppJs(paths, compileConfig, restrictionPath, compilerMakeFile);

	U.copyFileSync(path.join(alloyRoot, 'template', 'alloy.bootstrap.js'), path.join(paths.resources, titaniumFolder, 'alloy.bootstrap.js'));

	// ALOY-905: workaround TiSDK < 3.2.0 iOS device build bug where it can't reference app.js
	// in platform-specific folders, so we just copy the platform-specific one to
	// the Resources folder.
	if (buildPlatform === 'ios' && tiapp.version.lt('3.2.0')) {
		U.copyFileSync(path.join(paths.resources, titaniumFolder, 'app.js'), path.join(paths.resources, 'app.js'));
	}

	// optimize code
	logger.info('----- OPTIMIZING -----');

	if (restrictionSkipOptimize) {
		logger.info('Skipping optimize due to file restriction.');
	} else {
		optimizeCompiledCode(alloyConfig, paths);
	}

	// trigger our custom compiler makefile
	if (compilerMakeFile.isActive) {
		compilerMakeFile.trigger('post:compile', _.clone(compileConfig));
	}

	// write out the log for this build
	buildLog.write();

	BENCHMARK('TOTAL', true);
};


///////////////////////////////////////
////////// private functions //////////
///////////////////////////////////////
function generateAppJs(paths, compileConfig, restrictionPath, compilerMakeFile) {
	var alloyJs = path.join(paths.app, 'alloy.js');

	if (restrictionPath !== null && !_.includes(restrictionPath, path.join(paths.app, 'alloy.js')) ) {
		// skip alloy.js processing when filtering on another file
		return;
	}

	// info needed to generate app.js
	var target = {
			filename: path.join('Resources', titaniumFolder, 'app.js'),
			filepath: path.join(paths.resources, titaniumFolder, 'app.js'),
			template: path.join(alloyRoot, 'template', 'app.js')
		},

		// additional data used for source mapping
		data = {
			'__MAPMARKER_ALLOY_JS__': {
				filename: 'app' + path.sep + 'alloy.js',
				filepath: alloyJs
			}
		},

		// hash used to determine if we need to rebuild
		hash = U.createHash(alloyJs);

	// is it already generated from a prior compile?
	buildLog.data[buildPlatform] || (buildLog.data[buildPlatform] = {});
	if (!compileConfig.buildLog.data.deploytypeChanged && fs.existsSync(target.filepath) && buildLog.data[buildPlatform][alloyJs] === hash) {
		logger.info('[app.js] using cached app.js...');
		restrictionSkipOptimize = (restrictionPath !== null);

	// if not, generate the platform-specific app.js and save its hash
	} else {
		logger.info('[app.js] Titanium entry point processing...');

		// trigger our custom compiler makefile
		if (compilerMakeFile.isActive) {
			compilerMakeFile.trigger('compile:app.js', _.clone(compileConfig));
		}

		sourceMapper.generateCodeAndSourceMap({
			target: target,
			data: data,
		}, compileConfig);
		fileRestrictionUpdatedFiles.push(path.relative('Resources', target.filename));
		buildLog.data[buildPlatform][alloyJs] = hash;
	}

	buildLog.data[buildPlatform]['theme'] = theme;
	logger.info('');
}

function matchesRestriction(files, fileRestriction) {
	var matches = false;

	_.each(files, function(file) {
		if (typeof file === 'string') {
			matches |= _.includes(fileRestriction, file);
		} else if (typeof file === 'object') {
			// platform-specific TSS files result in an object
			// with a property of platform === true which needs
			// to be removed to prevent a compile error
			delete file.platform;
			matches |= matchesRestriction(file, fileRestriction);
		} else {
			throw 'Unsupported file type: ' + typeof file;
		}
	});

	return matches;
}

function parseAlloyComponent(view, dir, manifest, noView, fileRestriction) {
	var parseType = noView ? 'controller' : 'view';
	fileRestriction = fileRestriction || null;

	// validate parameters
	if (!view) { U.die('Undefined ' + parseType + ' passed to parseAlloyComponent()'); }
	if (!dir) { U.die('Failed to parse ' + parseType + ' "' + view + '", no directory given'); }

	var dirRegex = new RegExp('^(?:' + CONST.PLATFORM_FOLDERS_ALLOY.join('|') + ')[\\\\\\/]*');
	var basename = path.basename(view, '.' + CONST.FILE_EXT[parseType.toUpperCase()]),
		dirname = path.dirname(view).replace(dirRegex, ''),
		viewName = basename,
		template = {
			viewCode: '',
			modelVariable: CONST.BIND_MODEL_VAR,
			parentVariable: CONST.PARENT_SYMBOL_VAR,
			itemTemplateVariable: CONST.ITEM_TEMPLATE_VAR,
			controllerPath: (dirname ? path.join(dirname, viewName) : viewName).replace(/\\/g, '/'),
			preCode: '',
			postCode: '',
			Widget: !manifest ? '' : 'var ' + CONST.WIDGET_OBJECT +
				" = new (require('/alloy/widget'))('" + manifest.id + "');this.__widgetId='" +
				manifest.id + "';",
			WPATH: !manifest ? '' : _.template(fs.readFileSync(path.join(alloyRoot, 'template', 'wpath.js'), 'utf8'))({ WIDGETID: manifest.id }),
			__MAPMARKER_CONTROLLER_CODE__: '',
			ES6Mod: ''
		},
		widgetDir = dirname ? path.join(CONST.DIR.COMPONENT, dirname) : CONST.DIR.COMPONENT,
		widgetStyleDir = dirname ? path.join(CONST.DIR.RUNTIME_STYLE, dirname) :
			CONST.DIR.RUNTIME_STYLE,
		state = { parent: {}, styles: [] },
		files = {};

	// reset the bindings map
	styler.bindingsMap = {};
	CU.destroyCode = '';
	CU.postCode = '';
	CU[CONST.AUTOSTYLE_PROPERTY] = compileConfig[CONST.AUTOSTYLE_PROPERTY];
	CU.currentManifest = manifest;
	CU.currentDefaultId = viewName;

	// create a list of file paths
	var searchPaths = noView ? ['CONTROLLER'] : ['VIEW', 'STYLE', 'CONTROLLER'];
	_.each(searchPaths, function(fileType) {
		// get the path values for the file
		var fileTypeRoot = path.join(dir, CONST.DIR[fileType]);
		var filename = viewName + '.' + CONST.FILE_EXT[fileType];
		var filepath = dirname ? path.join(dirname, filename) : filename;

		// check for platform-specific versions of the file
		var baseFile = path.join(fileTypeRoot, filepath);
		if (buildPlatform) {
			var platformSpecificFile = path.join(fileTypeRoot, buildPlatform, filepath);
			if (fs.existsSync(platformSpecificFile)) {
				if (fileType === 'STYLE') {
					files[fileType] = [
						{ file:baseFile },
						{ file:platformSpecificFile, platform:true }
					];
				} else {
					files[fileType] = platformSpecificFile;
				}
				return;
			}
		}
		files[fileType] = baseFile;
	});

	if (fileRestriction !== null && !matchesRestriction(files, fileRestriction)) {
		logger.info('  Not matching the file restriction, skipping');
		return;
	}

	_.each(['COMPONENT', 'RUNTIME_STYLE'], function(fileType) {
		files[fileType] = path.join(compileConfig.dir.resources, 'alloy', CONST.DIR[fileType]);
		if (dirname) { files[fileType] = path.join(files[fileType], dirname); }
		files[fileType] = path.join(files[fileType], viewName + '.js');
	});

	// we are processing a view, not just a controller
	if (!noView) {
		// validate view
		if (!fs.existsSync(files.VIEW)) {
			logger.warn('No ' + CONST.FILE_EXT.VIEW + ' view file found for view ' + files.VIEW);
			return;
		}

		// load global style, if present
		state.styles = styler.globalStyle || [];

		// Load the style and update the state
		if (files.STYLE) {
			var theStyles = _.isArray(files.STYLE) ? files.STYLE : [{file:files.STYLE}];
			_.each(theStyles, function(style) {
				if (fs.existsSync(style.file)) {
					logger.info('  style:      "' +
						path.relative(path.join(dir, CONST.DIR.STYLE), style.file) + '"');
					state.styles = styler.loadAndSortStyle(style.file, {
						existingStyle: state.styles,
						platform: style.platform
					});
				}
			});
		}

		if (theme) {
			// if a theme is applied, override TSS definitions with those defined in the theme
			var themeStylesDir, theStyle, themeStylesFile, psThemeStylesFile;
			if (!manifest) {
				// theming a "normal" controller
				themeStylesDir = path.join(compileConfig.dir.themes, theme, 'styles');
				theStyle = dirname ? path.join(dirname, viewName + '.tss') : viewName + '.tss';
				themeStylesFile = path.join(themeStylesDir, theStyle);
				psThemeStylesFile = path.join(themeStylesDir, buildPlatform, theStyle);
			} else {
				// theming a widget
				themeStylesDir = path.join(compileConfig.dir.themes, theme, 'widgets', manifest.id, 'styles');
				theStyle = dirname ? path.join(dirname, viewName + '.tss') : viewName + '.tss';
				themeStylesFile = path.join(themeStylesDir, theStyle);
				psThemeStylesFile = path.join(themeStylesDir, buildPlatform, theStyle);
			}

			if (fs.existsSync(themeStylesFile)) {
				// load theme-specific styles, overriding default definitions
				logger.info('  theme:      "' + path.join(theme.toUpperCase(), theStyle) + '"');
				state.styles = styler.loadAndSortStyle(themeStylesFile, {
					existingStyle: state.styles,
					theme: true
				});
			}
			if (fs.existsSync(psThemeStylesFile)) {
				// load theme- and platform-specific styles, overriding default definitions
				logger.info('  theme:      "' +
					path.join(theme.toUpperCase(), buildPlatform, theStyle) + '"');
				state.styles = styler.loadAndSortStyle(psThemeStylesFile, {
					existingStyle: state.styles,
					platform: true,
					theme: true
				});
			}
		}

		// Load view from file into an XML document root node
		var docRoot;
		try {
			logger.info('  view:       "' +
				path.relative(path.join(dir, CONST.DIR.VIEW), files.VIEW) + '"');
			docRoot = U.XML.getAlloyFromFile(files.VIEW);
		} catch (e) {
			U.die([
				e.stack,
				'Error parsing XML for view "' + view + '"'
			]);
		}

		// see if autoStyle is enabled for the view
		if (docRoot.hasAttribute(CONST.AUTOSTYLE_PROPERTY)) {
			CU[CONST.AUTOSTYLE_PROPERTY] =
				docRoot.getAttribute(CONST.AUTOSTYLE_PROPERTY) === 'true';
		}

		// see if module attribute has been set on the docRoot (<Alloy>) tag for the view
		if (docRoot.hasAttribute(CONST.DOCROOT_MODULE_PROPERTY)) {
			CU[CONST.DOCROOT_MODULE_PROPERTY] = docRoot.getAttribute(CONST.DOCROOT_MODULE_PROPERTY);
		} else {
			CU[CONST.DOCROOT_MODULE_PROPERTY] = null;
		}

		// see if baseController attribute has been set on the docRoot (<Alloy>) tag for the view
		if (docRoot.hasAttribute(CONST.DOCROOT_BASECONTROLLER_PROPERTY)) {
			CU[CONST.DOCROOT_BASECONTROLLER_PROPERTY] = '"' + docRoot.getAttribute(CONST.DOCROOT_BASECONTROLLER_PROPERTY) + '"';
		} else {
			CU[CONST.DOCROOT_BASECONTROLLER_PROPERTY] = null;
		}

		// make sure we have a Window, TabGroup, or SplitWindow
		var rootChildren = U.XML.getElementsFromNodes(docRoot.childNodes);
		if (viewName === 'index' && !dirname) {
			var valid = [
				'Ti.UI.Window',
				'Ti.UI.iOS.SplitWindow',
				'Ti.UI.TabGroup',
				'Ti.UI.iOS.NavigationWindow',
				'Ti.UI.NavigationWindow'
			].concat(CONST.MODEL_ELEMENTS);
			_.each(rootChildren, function(node) {
				var found = true;
				var args = CU.getParserArgs(node, {}, { doSetId: false });

				if (args.fullname === 'Alloy.Require') {
					var inspect = CU.inspectRequireNode(node);
					for (var j = 0; j < inspect.names.length; j++) {
						if (!_.includes(valid, inspect.names[j])) {
							found = false;
							break;
						}
					}
				} else {
					found = _.includes(valid, args.fullname);
				}

				if (!found) {
					U.die([
						'Compile failed. index.xml must have a top-level container element.',
						'Valid elements: [' + valid.join(',') + ']'
					]);
				}
			});
		}

		// process any model/collection nodes
		_.each(rootChildren, function(node, i) {
			var fullname = CU.getNodeFullname(node);
			var isModelElement = _.includes(CONST.MODEL_ELEMENTS, fullname);

			if (isModelElement) {
				var vCode = CU.generateNode(node, state, undefined, false, true);
				template.viewCode += vCode.content;
				template.preCode += vCode.pre;

				// remove the model/collection nodes when done
				docRoot.removeChild(node);
			}
		});

		// rebuild the children list since model elements have been removed
		rootChildren = U.XML.getElementsFromNodes(docRoot.childNodes);

		// process the UI nodes
		_.each(rootChildren, function(node, i) {
			// should we use the default id?
			var defaultId = CU.isNodeForCurrentPlatform(node) ? viewName : undefined;

			// generate the code for this node
			var fullname = CU.getNodeFullname(node);
			template.viewCode += CU.generateNode(node, {
				parent:{},
				styles:state.styles,
				widgetId: manifest ? manifest.id : undefined,
				parentFormFactor: node.hasAttribute('formFactor') ? node.getAttribute('formFactor') : undefined
			}, defaultId, true);
		});
	}

	// process the controller code
	if (fs.existsSync(files.CONTROLLER)) {
		logger.info('  controller: "' +
			path.relative(path.join(dir, CONST.DIR.CONTROLLER), files.CONTROLLER) + '"');
	}
	var cCode = CU.loadController(files.CONTROLLER);
	template.parentController = (cCode.parentControllerName !== '') ?
		cCode.parentControllerName : CU[CONST.DOCROOT_BASECONTROLLER_PROPERTY] || "'BaseController'";
	template.__MAPMARKER_CONTROLLER_CODE__ += cCode.controller;
	template.preCode += cCode.pre;
	template.ES6Mod += cCode.es6mods;

	// for each model variable in the bindings map...
	_.each(styler.bindingsMap, function(mapping, modelVar) {

		// open the model binding handler
		var handlerVar = CU.generateUniqueId();
		template.viewCode += 'var ' + handlerVar + ' = function() {';

		_.each(mapping.models, function(modelVar) {
			template.viewCode += modelVar + '.__transform = _.isFunction(' + modelVar + '.transform) ? ' + modelVar + '.transform() : ' + modelVar + '.toJSON();';
		});

		CU.destroyCode += modelVar + ' && ' + ((state.parentFormFactor) ? 'is' + U.ucfirst(state.parentFormFactor) : '' ) +
			modelVar + ".off('" + CONST.MODEL_BINDING_EVENTS + "'," + handlerVar + ');';

		// for each specific conditional within the bindings map....
		_.each(_.groupBy(mapping.bindings, function(b) {return b.condition;}), function(bindings, condition) {
			var bCode = '';

			// for each binding belonging to this model/conditional pair...
			_.each(bindings, function(binding) {
				bCode += '$.' + binding.id + '.' + binding.prop + ' = ' + binding.val + ';';
			});

			// if this is a legit conditional, wrap the binding code in it
			if (typeof condition !== 'undefined' && condition !== 'undefined') {
				bCode = 'if(' + condition + '){' + bCode + '}';
			}
			template.viewCode += bCode;

		});
		template.viewCode += '};';
		template.viewCode += modelVar + ".on('" + CONST.MODEL_BINDING_EVENTS + "'," +
			handlerVar + ');';
	});

	// add destroy() function to view for cleaning up bindings
	template.viewCode += 'exports.destroy = function () {' + CU.destroyCode + '};';

	// add dataFunction of original name (if data-binding with form factor has been used)
	if (!_.isEmpty(CU.dataFunctionNames)) {
		_.each(Object.keys(CU.dataFunctionNames), function(funcName) {
			template.viewCode += 'function ' + funcName + '() { ';
			_.each(CU.dataFunctionNames[funcName], function(formFactor) {
				template.viewCode += '	if(Alloy.is' + U.ucfirst(formFactor) + ') { ' + funcName + U.ucfirst(formFactor) + '(); } ';
			});
			template.viewCode += '}';
		});
	}

	// add any postCode after the controller code
	template.postCode += CU.postCode;

	// create generated controller module code for this view/controller or widget
	var controllerCode = template.__MAPMARKER_CONTROLLER_CODE__;
	delete template.__MAPMARKER_CONTROLLER_CODE__;
	var code = _.template(fs.readFileSync(path.join(compileConfig.dir.template, 'component.js'), 'utf8'))(template);

	// prep the controller paths based on whether it's an app
	// controller or widget controller
	var targetFilepath = path.join(compileConfig.dir.resources, titaniumFolder,
		path.relative(compileConfig.dir.resources, files.COMPONENT));
	var runtimeStylePath = path.join(compileConfig.dir.resources, titaniumFolder,
		path.relative(compileConfig.dir.resources, files.RUNTIME_STYLE));
	if (manifest) {
		fs.mkdirpSync(
			path.join(compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET,
				manifest.id, widgetDir)
		);
		fs.mkdirpSync(
			path.join(compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET,
				manifest.id, widgetStyleDir)
		);

		// [ALOY-967] merge "i18n" dir in widget folder
		CU.mergeI18N(path.join(dir, 'i18n'), path.join(compileConfig.dir.project, 'i18n'), { override: false });
		widgetIds.push(manifest.id);

		CU.copyWidgetResources(
			[path.join(dir, CONST.DIR.ASSETS), path.join(dir, CONST.DIR.LIB)],
			path.join(compileConfig.dir.resources, titaniumFolder),
			manifest.id,
			{
				filter: new RegExp('^(?:' + otherPlatforms.join('|') + ')[\\/\\\\]'),
				exceptions: otherPlatforms,
				titaniumFolder: titaniumFolder,
				theme: theme
			}
		);
		targetFilepath = path.join(
			compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET, manifest.id,
			widgetDir, viewName + '.js'
		);
		runtimeStylePath = path.join(
			compileConfig.dir.resources, titaniumFolder, 'alloy', CONST.DIR.WIDGET, manifest.id,
			widgetStyleDir, viewName + '.js'
		);
	}

	// generate the code and source map for the current controller
	sourceMapper.generateCodeAndSourceMap({
		target: {
			filename: path.relative(compileConfig.dir.project, files.COMPONENT),
			filepath: targetFilepath,
			templateContent: code
		},
		data: {
			__MAPMARKER_CONTROLLER_CODE__: {
				filename: path.relative(compileConfig.dir.project, files.CONTROLLER),
				fileContent: controllerCode
			}
		}
	}, compileConfig);

	// initiate runtime style module creation
	var relativeStylePath = path.relative(compileConfig.dir.project, runtimeStylePath);
	logger.info('  created:     "' + relativeStylePath + '"');

	// skip optimize process, as the file is an alloy component
	restrictionSkipOptimize = (fileRestriction !== null);

	// pre-process runtime controllers to save runtime performance
	var STYLE_PLACEHOLDER = '__STYLE_PLACEHOLDER__';
	var STYLE_REGEX = new RegExp('[\'"]' + STYLE_PLACEHOLDER + '[\'"]');
	var processedStyles = [];
	_.each(state.styles, function(s) {
		var o = {};

		// make sure this style entry applies to the current platform
		if (s && s.queries && s.queries.platform &&
			!_.includes(s.queries.platform, buildPlatform)) {
			return;
		}

		// get the runtime processed version of the JSON-safe style
		var processed = '{' + styler.processStyle(s.style, state) + '}';

		// create a temporary style object, sans style key
		_.each(s, function(v, k) {
			if (k === 'queries') {
				var queriesObj = {};

				// optimize style conditionals for runtime
				_.each(s[k], function(query, queryKey) {
					if (queryKey === 'platform') {
						// do nothing, we don't need the platform key anymore
					} else if (queryKey === 'formFactor') {
						queriesObj[queryKey] = 'is' + U.ucfirst(query);
					} else if (queryKey === 'if') {
						queriesObj[queryKey] =  query;
					} else {
						logger.warn('Unknown device query "' + queryKey + '"');
					}
				});

				// add the queries object, if not empty
				if (!_.isEmpty(queriesObj)) {
					o[k] = queriesObj;
				}
			} else if (k !== 'style') {
				o[k] = v;
			}
		});

		// Create a full processed style string by inserting the processed style
		// into the JSON stringifed temporary style object
		o.style = STYLE_PLACEHOLDER;
		processedStyles.push(JSON.stringify(o).replace(STYLE_REGEX, processed));
	});

	// write out the pre-processed styles to runtime module files
	var styleCode = 'module.exports = [' + processedStyles.join(',') + '];';
	if (manifest) {
		styleCode += _.template(fs.readFileSync(path.join(alloyRoot, 'template', 'wpath.js'), 'utf8'))({ WIDGETID: manifest.id });
	}
	fs.mkdirpSync(path.dirname(runtimeStylePath));
	fs.writeFileSync(runtimeStylePath, styleCode);
}

function findModelMigrations(name, inDir) {
	try {
		var migrationsDir = inDir || compileConfig.dir.migrations;
		var files = fs.readdirSync(migrationsDir);
		var part = '_' + name + '.' + CONST.FILE_EXT.MIGRATION;

		// look for our model
		files = _.reject(files, function(f) { return f.indexOf(part) === -1; });

		// sort them in the oldest order first
		files = files.sort(function(a, b) {
			var x = a.substring(0, a.length - part.length - 1);
			var y = b.substring(0, b.length - part.length - 1);
			if (x < y) { return -1; }
			if (x > y) { return 1; }
			return 0;
		});

		var codes = [];
		_.each(files, function(f) {
			var mf = path.join(migrationsDir, f);
			var m = fs.readFileSync(mf, 'utf8');
			var code = '(function(migration){\n ' +
				"migration.name = '" + name + "';\n" +
				"migration.id = '" + f.substring(0, f.length - part.length).replace(/_/g, '') + "';\n" +
				m +
				'})';
			codes.push(code);
		});
		logger.info('Found ' + codes.length + ' migrations for model: ' + name);
		return codes;
	} catch (E) {
		return [];
	}
}

function processModels(dirs) {
	var models = [];
	var modelTemplateFile = path.join(alloyRoot, 'template', 'model.js');

	_.each(dirs, function(dirObj) {
		var modelDir = path.join(dirObj.dir, CONST.DIR.MODEL);
		if (!fs.existsSync(modelDir)) {
			return;
		}

		var migrationDir = path.join(dirObj.dir, CONST.DIR.MIGRATION);
		var manifest = dirObj.manifest;
		var isWidget = typeof manifest !== 'undefined' && manifest !== null;
		var pathPrefix = isWidget ? 'widgets/' + manifest.id + '/' : '';
		_.each(fs.readdirSync(modelDir), function(file) {
			if (!modelRegex.test(file)) {
				logger.warn('Non-model file "' + file + '" in ' + pathPrefix + 'models directory');
				return;
			}
			logger.info('[' + pathPrefix + 'models/' + file + '] model processing...');

			var fullpath = path.join(modelDir, file);
			var basename = path.basename(fullpath, '.' + CONST.FILE_EXT.MODEL);

			// generate model code based on model.js template and migrations
			var code = _.template(fs.readFileSync(modelTemplateFile, 'utf8'))({
				basename: basename,
				modelJs: fs.readFileSync(fullpath, 'utf8'),
				migrations: findModelMigrations(basename, migrationDir)
			});

			// write the model to the runtime file
			var casedBasename = U.properCase(basename);
			var modelRuntimeDir = path.join(compileConfig.dir.resources,
				titaniumFolder, 'alloy', 'models');
			if (isWidget) {
				modelRuntimeDir = path.join(compileConfig.dir.resources,
					titaniumFolder, 'alloy', 'widgets', manifest.id, 'models');
			}
			fs.mkdirpSync(modelRuntimeDir);
			fs.writeFileSync(path.join(modelRuntimeDir, casedBasename + '.js'), code);
			models.push(basename);
		});
	});

	return models;
}

function updateFilesWithBuildLog(src, dst, opts) {
	// filter on retrictionPath
	if (opts.restrictionPath === null || _.find(opts.restrictionPath, function(f) {return f.indexOf(src) === 0;})) {
		var updatedFiles = U.updateFiles(src, dst, _.extend({ isNew: buildLog.isNew }, opts));

		if (typeof updatedFiles == 'object' && updatedFiles.length > 0 && opts.restrictionPath !== null) {
			fileRestrictionUpdatedFiles = _.union(fileRestrictionUpdatedFiles, updatedFiles);
		}
	}
}

function optimizeCompiledCode(alloyConfig, paths) {
	var lastFiles = [],
		files;

	// Get the list of JS files from the Resources directory
	// and exclude files that don't need to be optimized, or
	// have already been optimized.
	function getJsFiles() {
		if (alloyConfig.file && (fileRestrictionUpdatedFiles.length > 0)) {
			logger.info('Restricting optimize on file(s) : ' + fileRestrictionUpdatedFiles.join(', '));
			return fileRestrictionUpdatedFiles;
		}

		var exceptions = [
			'app.js',
			'alloy/CFG.js',
			'alloy/controllers/',
			'alloy/styles/',
			'alloy/backbone.js',
			'alloy/constants.js',
			'alloy/underscore.js',
			'alloy/widget.js'
		].concat(compileConfig.optimizingExceptions || []);

		// widget controllers are already optimized. It should be listed in exceptions.
		_.each(compileConfig.dependencies, function (version, widgetName) {
			exceptions.push('alloy/widgets/' + widgetName + '/controllers/');
		});

		_.each(exceptions.slice(0), function(ex) {
			exceptions.push(`${titaniumFolder}/${ex}`);
		});

		var excludePatterns = otherPlatforms.concat(['.+node_modules']);
		var rx = new RegExp('^(?!' + excludePatterns.join('|') + ').+\\.js$');
		return _.filter(walkSync(compileConfig.dir.resources), function(f) {
			return rx.test(f) && !_.find(exceptions, function(e) {
				return f.indexOf(e) === 0;
			}) && !fs.statSync(path.join(compileConfig.dir.resources, f)).isDirectory();
		});
	}

	while ((files = _.difference(getJsFiles(), lastFiles)).length > 0) {
		_.each(files, function(file) {
			var options = _.extend(_.clone(sourceMapper.OPTIONS_OUTPUT), {
					plugins: [
						[require('./ast/builtins-plugin'), compileConfig],
						[require('./ast/optimizer-plugin'), compileConfig.alloyConfig],
					]
				}),
				fullpath = path.join(compileConfig.dir.resources, file);

			logger.info('- ' + file);
			try {
				var result = babel.transformFileSync(fullpath, options);
				fs.writeFileSync(fullpath, result.code);
			} catch (e) {
				U.die('Error transforming JS file', e);
			}
		});
		lastFiles = _.union(lastFiles, files);
	}
}

function BENCHMARK(desc, isFinished) {
	var places = Math.pow(10, 5);
	desc = desc || '<no description>';
	if (times.first === null) {
		times.first = process.hrtime();
		return;
	}

	function hrtimeInSeconds(t) {
		return t[0] + (t[1] / 1000000000);
	}

	var total = process.hrtime(times.first);
	var current = hrtimeInSeconds(total) - (times.last ? hrtimeInSeconds(times.last) : 0);
	times.last = total;
	var thisTime = Math.round((isFinished ? hrtimeInSeconds(total) : current) * places) / places;
	times.msgs.push('[' + thisTime + 's] ' + desc);
	if (isFinished) {
		logger.trace(' ');
		logger.trace('Benchmarking');
		logger.trace('------------');
		logger.trace(times.msgs);
		logger.info('');
		logger.info('Alloy compiled in ' + thisTime + 's');
	}
}
